{wcademy}

Как организовать проект на go?

May 11, 2020

Новичкам в go часто выносит голову вопрос организации гошного проекта. Если разбирать вопрос подробно, то он включает следующие подпункты:

  • Как структура моего репозитория определяет то, как пользователи будут импортировать мой код?
  • Как мне распространять консольные утилиты в дополнение к коду?
  • Как go-модули изменили структуру проектов?
  • Как несколько пакетов сосуществуют в одном модуле?

Ещё сложности добавляет то, что подходы, которые легче всего нагуглить являются либо устаревшими (уже давно отпала необходимость в том, чтобы держать код проектов в $GOPATH/src 👴) или переусложненными на пустом месте (golang-standards/project-layout - замечательный вариант организации кода большого проекта, но именно БОЛЬШОГО ПРОЕКТА). И во всём этом очень легко увязнуть, как только программа перестаёт помещаться в один main.go. Я хочу предложить очень простой, но расширяемый и, главное, актуальный шаблон проекта.

В этот туториале я остановлюсь на следующем:

  • Как разбивать проект на пакеты, что бы их могли импортировать как другие пакеты проекта, так и пользователи (если у нас библиотека)
  • Как использовать внутренние (internal) пакеты, которые могут быть импортированы только изнутри проекта, но не сторонними пользователями
  • Возможность кому угодно поставить нашу отдельные программы простым go get

Введение

Для примера, мы создадим очень простую математическую библиотеку для целых чисел. Она будет состоять из двух консольных утилит, одна позволяет сложить переданные на вход числа, вторая перемножить. Также, предполагается, что этими же возможностями сможет воспользоваться пользователь нашего модуля, просто импортировав его себе в проект.

Готовый проект - https://github.com/wcademy/simplegoprojectlayout.

Прежде всего об имени проекта, немало копий сломано, но я рекомендую использовать в качестве имени проекта адрес репозитория. В нашем случае github.com/wcademy/simplegoprojectlayout. Всегда. Неважно, это приватный проект, или его будут размещать публично. Единственное исключение из этого — если это одноразовый скрипт, который никуда не уйдёт с вашего компьютера, но зачем тогда вообще читать об организации проекта? Пишите в одном файле.

Чтобы инициализировать проект с модулями запускаем go mod: go mod init github.com/wcademy/simplegoprojectlayout

Имя проекта крайне важно, оно служит базовым путём для импорта:

import "github.com/wcademy/simplegoprojectlayout/sumlib"
        имя модуля                              /имя пакета

Организация проекта

.
├── cmd
│   ├── example
│   │   └── main.go
│   ├── multiply
│   │   └── main.go
│   └── sum
│       └── main.go
├── internal
│   └── comandlineutils
│       ├── args.go
│       ├── args_test.go
│       └── errors.go
├── multiplylib
│   ├── multiply.go
│   └── multiply_test.go
├── sumlib
│   ├── sum.go
│   └── sum_test.go
├── .gitignore
├── .golangci.yaml
├── LICENSE
├── README.md
├── go.mod
├── go.sum
└── ints.go

.gitignore — не забывайте добавлять .gitignore в каждый проект, не допускайте сохранения в истории гита бинарников, файлов настроек IDE, кешей, приватных настроек.

.golangci.yaml — файл конфигурации golangci-lint, сам по себе необязателен, а вот проверка и исправление замечаний линтера — да, нужно.

LICENSE — очень важный файл. Если вы публикуете что-то, и вы не против, чтобы это кто-то использовал, обязательно добавьте опенсурсную лицензию в репозиторий вроде MIT или Apache License. Если нет лицензии — никто не вправе использовать ваш код.

README.md - ещё один очень важный файл. Пожалуйста, всегда добавляйте его в проект. В нём вкратце опишите для чего он нужен и как его использовать.

go.mod — файл описывающий модуль, он содержит имя проекта, версию Go с которой он был создан, а также список прямых зависимостей. Он создался, когда мы вызвали “go mod init”.

go.sum содержит список ВСЕХ зависимостей модуля, их точные версии и их контрольные суммы. Он управляется go mod автоматически, и руками его трогать не нужно. Он появится после вызова go mod tidy, чтобы подтянуть новые зависимости.

ints.go — наконец добрались до первого go-файла. В корень проекта файлы с кодом добавляются исключительно если этот модуль предполагается использовать как библиотеку, для того чтобы обеспечить краткий путь импорта github.com/wcademy/simplegoprojectlayout. Причём только самые общие для модуля сущности. Если ваш модуль не библиотека, а приложение, то в корень лучше вообще не добавлять go-файлы. В нашем примере:

package simplegoprojectlayout

//  Ints представляет собой слайс целых чисел
type Ints []int

// SliceToInts преобразует слайс интов во внутренний тип данных Ints
func SliceToInts(slice []int) Ints {
	ints := make(Ints, len(slice))

	for i := range slice {
		ints[i] = slice[i]
	}

	return slice
}

Мы определили в нём общую структуру данных для всех остальных математических подпакетов нашего модуля. Ещё стоит обратить внимание на строку package simplegoprojectlayout. Так как файл в корне модуля, имя его пакета должно совпадать с последней частью имени модуля, чтобы не вводить в заблуждение пользователей модуля. Когда что-то импортируется под именем github.com/wcademy/simplegoprojectlayout, ожидается, что это можно будет использовать под именем simplegoprojectlayout, а не рандомным наименованием:

package main

import (
	"fmt"

	"github.com/wcademy/simplegoprojectlayout"
	"github.com/wcademy/simplegoprojectlayout/sumlib"
)

func main() {
	ints := simplegoprojectlayout.Ints{1, 2, 3}
	result := sumlib.Sum(ints)
	fmt.Println(result)
}

Публичные пакеты

Перейдём к папке multiplylib. Его можно импортировать по пути github.com/wcademy/simplegoprojectlayout/multiplylib. Этот пакет содержит только один файл - multiplylib/multiply.go. Их могло бы быть любое количество, но для примера хватит и одного. Названия файлов внутри пакетов ни на что не влияют, но всё-таки их названия должны отражать их содержимое. Всё, что в них определено, будет импортироваться по имени пакета.

sumlib — другой пакет, который может быть импортирован пользователем. Он ничем не отличается от первого, но мне хотелось показать работу с парой пакетов.

И пару слов о вложенных пакетах. Уровней вложенности может быть сколько угодно. Имя пакета, которое видит пользователь определяется относительным путём от корня модуля. К примеру, если бы у нас в корне лежал пакет с путём ./math/advanced/complex/sum, то его можно было бы импортировать по пути github.com/wcademy/simplegoprojectlayout/math/advanced/complex/sum и использовать как sum.

Также важно понять, что не нужно переусердствовать, как правило, достаточно одного уровня вложенности.

Команды/программы

Некоторые гошные проекты включают в себя программы, в дополнение к библиотекам (или, вообще, представляют собой только какую-то программу). Если у вас только библиотека, без запускаемого можете пропустить этот раздел.

Принято, что все программы, которые предоставляет проект лежат в подпапке ./cmd. Типичная схема именования:

github.com/wcademy/simplegoprojectlayout/cmd/cmd-name 
имя модуля                              /cmd/имя-команды

Такие программы могут быть установлены с помощью go get:

go get github.com/wcademy/simplegoprojectlayout/cmd/sum

Если выполнить эту программу, go скачает содержимое github.com/wcademy/simplegoprojectlayout, скомпилирует пакет sum в бинарник sum и разместит его в $GOPATH/bin. Если вы используете гошные утилиты, то хорошей идеей будет добавить этот путь в системный PATH.

Если вы не задавали явно GOPATH, то дефолтное расположение этой папки:

  • $HOME/go на маках и линуксах
  • %USERPROFILE%\go в венде

В нашем проекте две утилиты: sum и multiply. Go понимает, что это нечто запускаемое, так как код в них определён в пакете main. Также и сам код написан в файлах main.go, но это неважно, скорее так принято.

Так как у нас живой репозиторий, мы можем установить, к примеру, программу sum и она нам просуммирует переданные числа:

> get github.com/wcademy/simplegoprojectlayout/cmd/sum
> ~/go/bin/sum 1 2 3
6

Заглянем в одну из наших программ:

package main

import (
	"fmt"
	"os"

	"go.uber.org/zap"

	cu "github.com/wcademy/simplegoprojectlayout/internal/comandlineutils"
	"github.com/wcademy/simplegoprojectlayout/sumlib"
)

func main() {
	logger, err := zap.NewProduction()
	if err != nil {
		fmt.Println("can't init logger:", err)
	}

	ints, err := cu.GetArgs(os.Args)
	if err != nil {
		logger.Error("can't get args", zap.Error(err))
		return
	}

	result := sumlib.Sum(ints)

	fmt.Println(result)
}

В go используются абсолютные пути импорта, относительно корня проекта. Обратите внимание на то, как они происходят в этом пакете. Это применимо к любым пакетам, не только командам.

Внутренние пакеты

Другой важный концепт, который применяется в Go — внутренние (или приватные пакеты). Они могут быть импортированы только из другого пакета того же модуля, но не снаружи. Важно понимать, что всё, что может быть импортировано из модуля, становится его публичным API и должно быть максимально стабильным и неизменяемым (ну или изменяемым только в мажорных релизах). Поэтому очень важно, чтобы объём экспортируемого кода был как можно меньше, чтобы он скрывал своей абстракцией внутренние детали реализации, и её изменения не приводили к ломанию публичного API. Всё что может быть скрыто от глаз пользователя, должно быть скрыто внутри /internal/.

Go считает internal специальным путём, если кто-то попробует импортировать его содержимое из другого модуля, то получит ошибку:

use of internal package github.com/wcademy/simplegoprojectlayout/internal/comandlineutils not allowed

В нашем проекте внутри internal только один пакет — comandlineutils. Он служит для парсинга аргументов в наших командах, но он никак не связан с целью нашей математической библиотеки, поэтому я скрыл его реализацию внутри internal.

Я предпочитаю, как можно больше кода размещать внутри internal. Если сделать какое-то внутреннее API публичным можно небольшим автоматическим рефакторингом, то если потребуется перестать поддерживать какое-то уже публичное API, это будет болью и приведёт к большому количеству споров (а иногда обвинений), предупреждения пользователей сильно заранее. Если сомневаетесь в том, делать ли что-то внутренним или публичным, делайте внутренним.

В целом, всё. Структурируйте ваши проекты правильно 🎉.

🚀  Если узнал из статьи что-то полезное, ставь лайк и подписывайся на наш канал в Телеграм или группу ВК. Обсудить статью можно в нашем уютном чатике 😏

© 2019 - 2022, {wcademy}